Micron Document
🎖️GitЯра🎖️

Commit bd2863243bab6eb213401d949839a2bc74dde7e2


Parents : f3d8436
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-24T04:19:35-07:00
Committer : GitHub <noreply@github.com>
Date : 2026-08-24T06:19:35-05:00

fix(mqtt): tolerate object-typed payload in MQTT JSON messages (#6839)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttJsonPayload.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttJsonPayload.kt
index e6a6929c01..ceea81aa72 100644
--- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttJsonPayload.kt
+++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttJsonPayload.kt
@@ -18,6 +18,10 @@ package org.meshtastic.core.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable
data class MqttJsonPayload(
@@ -25,10 +29,17 @@ data class MqttJsonPayload(
val from: Long,
val to: Long? = null,
val channel: Int? = null,
- val payload: String? = null,
+ @Serializable(with = MqttPayloadStringSerializer::class) val payload: String? = null,
@SerialName("hop_limit") val hopLimit: Int? = null,
val id: Long? = null,
val time: Long? = null,
val sender: String? = null,
// Add other fields as needed for position/telemetry
)
+
+// Firmware and MQTT bridges send "payload" as a string for text messages but as a nested JSON
+// object for position/telemetry/map reports; coerce non-strings to their compact JSON text.
+private object MqttPayloadStringSerializer : JsonTransformingSerializer<String>(String.serializer()) {
+ override fun transformDeserialize(element: JsonElement): JsonElement =
+ if (element is JsonPrimitive && element.isString) element else JsonPrimitive(element.toString())
+}

diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/MqttJsonPayloadTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/MqttJsonPayloadTest.kt
new file mode 100644
index 0000000000..717c04f541
--- /dev/null
+++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/MqttJsonPayloadTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.model
+
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.Json
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+/** Locks down the tolerant "payload" decoding: string, nested object, and null/absent must all parse. */
+class MqttJsonPayloadTest {
+
+ // Matches the decode-relevant MQTTRepositoryImpl config.
+ private val json = Json { ignoreUnknownKeys = true }
+
+ @Test
+ fun string_payload_decodes_unchanged() {
+ val jsonStr =
+ """{"type":"text","from":12345678,"to":4294967295,"payload":"Hello World","hop_limit":3,"id":123}"""
+ val decoded = json.decodeFromString<MqttJsonPayload>(jsonStr)
+
+ assertEquals("text", decoded.type)
+ assertEquals(12345678L, decoded.from)
+ assertEquals("Hello World", decoded.payload)
+ }
+
+ @Test
+ fun object_payload_decodes_to_compact_json_text() {
+ val jsonStr =
+ """{"type":"position","from":12345678,"payload":{"latitude_i":123456789,"longitude_i":-987654321,"time":1600000000}}"""
+ val decoded = json.decodeFromString<MqttJsonPayload>(jsonStr)
+
+ assertEquals("position", decoded.type)
+ assertEquals("""{"latitude_i":123456789,"longitude_i":-987654321,"time":1600000000}""", decoded.payload)
+ }
+
+ @Test
+ fun null_payload_decodes_to_null() {
+ val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1,"payload":null}""")
+ assertNull(decoded.payload)
+ }
+
+ @Test
+ fun absent_payload_decodes_to_null() {
+ val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1}""")
+ assertNull(decoded.payload)
+ }
+
+ @Test
+ fun array_payload_decodes_to_compact_json_text() {
+ val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1,"payload":[1,2]}""")
+ assertEquals("[1,2]", decoded.payload)
+ }
+
+ @Test
+ fun non_string_primitive_payload_is_coerced_to_text() {
+ val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1,"payload":42}""")
+ assertEquals("42", decoded.payload)
+ }
+
+ @Test
+ fun string_payload_round_trips_as_string() {
+ val encoded =
+ json.encodeToString(MqttJsonPayload.serializer(), MqttJsonPayload(type = "text", from = 1, payload = "hi"))
+ assertTrue(encoded.contains(""""payload":"hi""""))
+ }
+
+ @Test
+ fun garbage_input_still_fails() {
+ assertFailsWith<SerializationException> { json.decodeFromString<MqttJsonPayload>("""{"from":"not json""") }
+ }
+}

Served by rngit 1.5.0 - Generated in 0.05s